﻿#region Copyright Notices
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Library General Public License for more details.
// 
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#endregion

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using Yarr;

namespace Yarr.YarrPad
{
    public class Editor : RichTextBox
    {
        #region Private members
        private static Yarr.Environment hostEnvironment;

        private Yarr.Environment environment;

        private Popup membersPopup;
        private List<String> shownMembers;
        private string memberFilter;

        protected TextPointer promptEnd;
        protected TextPointer initialPromptEnd;
        #endregion


        #region Public properties
        /// <summary>
        /// The Yarr environment in which to execute commands
        /// </summary>
        public virtual Yarr.Environment Environment
        {
            get { return environment; }
            set { environment = value; }
        }

        /// <summary>
        /// The codesense popup window
        /// </summary>
        public Popup MembersPopup
        {
            get
            {
                if (membersPopup == null)
                {
                    membersPopup = new Popup();
                    membersPopup.Child = new ListBox();
                    membersPopup.PlacementTarget = this;
                    membersPopup.Placement = PlacementMode.Relative;
                    MembersPopup.PreviewTextInput += OnMembersPopupPreviewTextInput;
                    MembersPopup.PreviewKeyDown += OnMembersPopupPreviewKeyDown;
                }
                return membersPopup;
            }
        }

        /// <summary>
        /// The text in the textbox
        /// </summary>
        public string Text
        {
            get
            {
                TextRange textRange = new TextRange(Document.ContentStart, Document.ContentEnd);
                return textRange.Text;
            }
        }
        #endregion


        #region Private properties
        private ListBox MembersListBox
        {
            get { return MembersPopup.Child as ListBox; }
        }
        #endregion


        #region Constructors
        public Editor()
        {
            if (hostEnvironment == null)
                hostEnvironment = Yarr.Environment.MakeEnvironment();

            this.environment = hostEnvironment;
        }

        public Editor(Yarr.Environment environment)
        {
            this.environment = environment;
        }
        #endregion


        #region Public methods
        /// <summary>
        /// Invoked when the parent's size or location changes
        /// </summary>
        public void SizeOrLocationChanged()
        {
            if (MembersPopup.IsOpen)
            {
                MembersPopup.IsOpen = false;
                Focus();
                MembersPopup.IsOpen = true;
                PositionMembersPopup();
                MembersListBox.Focus();
            }
        }

        /// <summary>
        /// Write text with specified color
        /// </summary>
        /// <param name="text">The text to write</param>
        /// <param name="type">The text color</param>
        public void Write(string text, TextType type)
        {
            BeginChange();

            Run run = new Run(text);
            run.Foreground = new SolidColorBrush(TextColors.GetColor(type));
            run.Background = Brushes.White;
            if (Document.Blocks.Count == 0)
                Document.Blocks.Add(new Paragraph());

            ((Paragraph)Document.Blocks.LastBlock).Inlines.Add(run);
            CaretPosition = run.ContentEnd;

            EndChange();
        }

        /// <summary>
        /// Write text with specfied color to new paragraph
        /// </summary>
        /// <param name="text">The text to write</param>
        /// <param name="type">The text color</param>
        public void WriteLine(string text, TextType type)
        {
            BeginChange();

            Run run = new Run(text);

            run.Foreground = new SolidColorBrush(TextColors.GetColor(type));
            run.Background = Brushes.White;

            Document.Blocks.Add(new Paragraph(run));
            CaretPosition = run.ContentEnd;

            EndChange();
        }
        #endregion


        #region Helper methods
        private TextPointer FindMatchingParen(TextPointer currPos)
        {
            currPos = InsertionPosition(currPos);

            Stack<TextPointer> lpars = new Stack<TextPointer>();
            TextPointer end = Document.ContentEnd.GetInsertionPosition(LogicalDirection.Backward);
            TextPointer pos = NextInsertionPosition(initialPromptEnd);

            char[] runBuff = new char[1];

            if (pos != null && currPos != null)
            {
                do
                {
                    if (pos != null)
                    {
                        TextPointer lpar;

                        pos.GetTextInRun(LogicalDirection.Forward, runBuff, 0, 1);

                        switch (runBuff[0])
                        {
                            case '"':
                                pos = SkipStringLiteral(pos);
                                break;

                            case '#':
                                pos = SkipCharLiteral(pos);
                                break;

                            case ';':
                                pos = InsertionPosition(pos.GetLineStartPosition(1));
                                break;

                            case '(':
                                lpars.Push(pos);
                                pos = NextInsertionPosition(pos);
                                break;

                            case ')':
                                if (lpars.Count == 0)
                                    return null;

                                lpar = lpars.Pop();
                                if (pos.CompareTo(currPos) == 0)
                                    return lpar;
                                if (lpar.CompareTo(currPos) == 0)
                                    return pos;

                                pos = NextInsertionPosition(pos);
                                break;

                            default:
                                pos = NextInsertionPosition(pos);
                                break;
                        }
                    }
                }
                while (pos != null && pos.CompareTo(end) < 0);
            }

            return null;
        }

        private void HandleCodeSense()
        {
            // popupFirstChar = true;
            memberFilter = String.Empty;

            TextRange range = new TextRange(CaretPosition.GetLineStartPosition(0), CaretPosition.GetPositionAtOffset(-1));

            string enteredText = range.Text;

            int lastSpaceIndex = enteredText.LastIndexOf(" ");
            if (lastSpaceIndex < 0)
                lastSpaceIndex = 0;

            int lastParenIndex = enteredText.LastIndexOf("(");
            if (lastParenIndex < 0)
                lastParenIndex = 0;

            int startPos = (lastParenIndex > lastSpaceIndex) ? lastParenIndex - 1 : lastSpaceIndex;

            string expression = enteredText.Substring(startPos + 1).Trim();

            string[] members = Runtime.GetMemberNames(expression, environment);

            if (members != null && members.Length > 0)
            {
                Array.Sort(members);
                shownMembers = new List<string>(members);
                ShowMembersList();
            }
        }

        protected void HandleException(Exception ex)
        {
            StringBuilder exception = new StringBuilder();
            exception.Append(ex.Message);
            if (exception.Length > 0)
                exception.Append(System.Environment.NewLine);

            WriteLine(exception.ToString(), TextType.Error);
        }

        private void HandleMemberSelect()
        {
            MembersPopup.IsOpen = false;
            Focus();
            InsertText(MembersListBox.SelectedItem.ToString());
        }

        private TextPointer InsertionPosition(TextPointer pos)
        {
            while (pos != null &&
                    pos.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text)
                pos = pos.GetNextInsertionPosition(LogicalDirection.Forward);

            return pos;
        }

        private void InsertText(string text)
        {
            BeginChange();

            if (Selection.Text != String.Empty)
                Selection.Text = String.Empty;

            if (text != string.Empty)
            {
                TextPointer tp = CaretPosition.GetPositionAtOffset(0, LogicalDirection.Forward);
                CaretPosition.InsertTextInRun(text);
                CaretPosition = tp;
            }

            EndChange();
        }

        protected TextPointer NextInsertionPosition(TextPointer pos)
        {
            return pos == null ? null :
                    InsertionPosition(pos.GetNextInsertionPosition(LogicalDirection.Forward));
        }

        private void PositionMembersPopup()
        {
            Rect rect = CaretPosition.GetCharacterRect(LogicalDirection.Forward);
            MembersPopup.HorizontalOffset = rect.Right;
            MembersPopup.VerticalOffset = rect.Top;
        }

        private void ShowMembersList()
        {
            MembersListBox.Items.Clear();

            foreach (string member in shownMembers)
            {
                if (String.IsNullOrEmpty(memberFilter) || 
                         member.StartsWith(memberFilter, StringComparison.OrdinalIgnoreCase))
                    MembersListBox.Items.Add(member);
            }

            if (MembersListBox.Items.Count > 0)
            {
                if (!MembersPopup.IsOpen)
                {
                    MembersPopup.IsOpen = true;
                    PositionMembersPopup();
                }

                MembersListBox.SelectedIndex = 0;
                MembersListBox.ScrollIntoView(MembersListBox.SelectedValue);
                MembersListBox.Focus();
            }
            else
            {
                MembersPopup.IsOpen = false;
                Focus();

                if (!String.IsNullOrEmpty(memberFilter))
                    InsertText(memberFilter);
            }
        }

        private TextPointer SkipCharLiteral(TextPointer pos)
        {
            pos = NextInsertionPosition(pos);

            if (pos != null)
            {
                char[] runBuff = new char[1];

                pos.GetTextInRun(LogicalDirection.Forward, runBuff, 0, 1);

                if (runBuff[0] == '\\')
                {
                    pos = NextInsertionPosition(pos);

                    if (pos != null)
                        pos = NextInsertionPosition(pos);
                }
                else if (runBuff[0] == '|')
                {
                    do
                    {
                        pos = NextInsertionPosition(pos);

                        if (pos != null)
                        {
                            pos.GetTextInRun(LogicalDirection.Forward, runBuff, 0, 1);

                            if (runBuff[0] == '|')
                            {
                                pos = NextInsertionPosition(pos);

                                pos.GetTextInRun(LogicalDirection.Forward, runBuff, 0, 1);

                                if (runBuff[0] == '#')
                                    return NextInsertionPosition(pos);
                            }
                        }
                    }
                    while (pos != null && pos.CompareTo(Document.ContentEnd) < 0);
                }
            }

            return pos;
        }

        private TextPointer SkipStringLiteral(TextPointer pos)
        {

            pos = NextInsertionPosition(pos);

            if (pos != null)
            {
                char[] runBuff = new char[1];

                do
                {
                    if (pos != null)
                    {
                        pos.GetTextInRun(LogicalDirection.Forward, runBuff, 0, 1);

                        switch (runBuff[0])
                        {
                            case '\\':
                                pos = NextInsertionPosition(pos);
                                if (pos != null)
                                    pos = NextInsertionPosition(pos);
                                break;

                            case '"':
                                return NextInsertionPosition(pos);

                            default:
                                pos = NextInsertionPosition(pos);
                                break;
                        }
                    }
                }
                while (pos != null && pos.CompareTo(Document.ContentEnd) < 0);
            }

            return pos;
        }
        #endregion


        #region Event handlers
        private void OnMembersPopupPreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            if (MembersPopup.IsOpen && !e.Handled)
            {
                bool popupFirstChar = String.IsNullOrEmpty(memberFilter);

                if (e.Text == "." || e.Text == ")")
                {
                    HandleMemberSelect();
                }
                else if (e.Text[0] == 0x08)
                {
                    e.Handled = true;
                    if (memberFilter.Length > 0)
                    {
                        memberFilter = memberFilter.Substring(0, memberFilter.Length - 1);
                        ShowMembersList();
                    }
                    else
                    {
                        membersPopup.IsOpen = false;
                        Focus();
                    }
                }
                else
                {
                    e.Handled = true;
                    memberFilter = memberFilter + e.Text;
                    ShowMembersList();
                }
            }
        }

        private void OnMembersPopupPreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (MembersPopup.IsOpen && !e.Handled)
            {
                if (e.Key == Key.Enter || e.Key == Key.Tab || e.Key == Key.Space)
                {
                    HandleMemberSelect();
                    if (e.Key == Key.Enter || e.Key == Key.Tab)
                    {
                        e.Handled = true;
                        InsertText(" ");
                    }
                }
                else if (e.Key == Key.Escape)
                {
                    membersPopup.IsOpen = false;
                    e.Handled = true;

                    Focus();
                    if (!String.IsNullOrEmpty(memberFilter))
                        InsertText(memberFilter);
                }
            }
        }
        
        protected override void OnPreviewKeyDown(KeyEventArgs e)
        {
            if (!e.Handled)
            {
                if (e.Key == Key.OemPeriod && Keyboard.Modifiers == ModifierKeys.None)
                {
                    e.Handled = true;
                    InsertText(".");
                    HandleCodeSense();
                }
                else if (e.Key == Key.Escape)
                {
                    if (MembersPopup.IsOpen)
                    {
                        membersPopup.IsOpen = false;
                        e.Handled = true;
                        Focus();
                    }
                }
                else if (e.Key == Key.Space)
                    new TextRange(initialPromptEnd, Document.ContentEnd).ClearAllProperties();
            }

            base.OnPreviewKeyDown(e);
        }

        protected override void OnPreviewTextInput(TextCompositionEventArgs e)
        {
            if (!e.Handled)
            {
                new TextRange(initialPromptEnd, Document.ContentEnd).ClearAllProperties();

                if (e.Text[0] == '(' || e.Text[0] == ')')
                {
                    e.Handled = true;
                    InsertText(e.Text);

                    TextPointer par2 =
                        FindMatchingParen(CaretPosition.GetNextInsertionPosition(LogicalDirection.Backward));

                    if (par2 != null)
                    {
                        new TextRange(CaretPosition
                                            .GetNextInsertionPosition(LogicalDirection.Backward),
                                      CaretPosition)
                            .ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.LightGray);

                        new TextRange(par2, par2.GetNextInsertionPosition(LogicalDirection.Forward))
                            .ApplyPropertyValue(TextElement.BackgroundProperty, Brushes.LightGray);
                    }
                }
            }

            base.OnPreviewTextInput(e);
        }
        #endregion
    }
}